package org.easypeas.http;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.easypeas.EasyPeasConstants;
import org.easypeas.exceptions.InvokerNotFoundException;
import org.easypeas.exceptions.ResourceInvocationFailure;
import org.easypeas.exceptions.UnableToDetermineTemplateException;
import org.easypeas.exceptions.UnknownResourceException;
import org.easypeas.exceptions.UnresolvedMethodException;
import org.easypeas.invocation.InvocationFactory;
import org.easypeas.invocation.Invoker;
import org.easypeas.mappings.Mappings;
import org.easypeas.mappings.MappingsFactory;
import org.easypeas.templates.TemplateFinder;
import org.easypeas.templates.TemplateFinderFactory;

/**
 * <p>
 * Main handler for handling EasyPea paths. Behaves as a singleton and should be
 * accessed via getInstance()
 * </p>
 * 
 * @author S Owen
 * 
 */
public class EasyPeasHandler {

	private static VelocityEngine engine = new VelocityEngine();

	// FIXME: Are these shared resources thread safe??
	private static Mappings mappings;

	private static Invoker invoker;

	private static TemplateFinder templateFinder;

	private static Logger logger = Logger.getLogger(EasyPeasHandler.class);

	private static EasyPeasHandler instance = new EasyPeasHandler();

	/**
	 * @return the EasyPeas singleton instance.
	 */
	public static EasyPeasHandler getInstance() {
		if (mappings == null) {
			logger
					.error("Mappings are not yet initailsed, you should call init(...) first");
		}
		if (invoker == null) {
			logger
					.error("Invoker is not yet initailsed, you should call init(...) first");
		}
		if (templateFinder == null) {
			logger
					.error("Template Finder is not yet initailsed, you should call init(...) first");
		}
		return instance;
	}

	/**
	 * Constructor, private to enforce singleton pattern
	 */
	private EasyPeasHandler() {

	}

	/**
	 * Initialises the handler with details from context.<br>
	 * This should be called before the first call to getInstance() and using an
	 * instance of the handler.
	 * 
	 * @param context the ServletContext
	 * @param templatePath the template path
	 *            the path to the templates directory, relative to the webapp
	 *            root.
	 * @throws ServletException if something goes wrong
	 */
	public static void init(ServletContext context, String templatePath)
			throws ServletException {

		templateFinder = TemplateFinderFactory.getInstance().finder();
		try {
			invoker = InvocationFactory.getInstance().invoker();
		} catch (InvokerNotFoundException e) {
			logger.error(e);
			invoker = e.getDefaultInvoker();
		}

		try {
			mappings = MappingsFactory.getInstance().mappings(
					context.getResource("/WEB-INF/easypeas.classes"));
		} catch (IOException e) {
			logger.error("Error creating mappings", e);
			throw new ServletException(e);
		}

		String templateResourcePath = context.getRealPath(templatePath);
		engine.addProperty("file.resource.loader.path", templateResourcePath);
	}

	/**
	 * Tests whether the path is one that should be handled by this handler.
	 * 
	 * @param path the path
	 * @param context the ServletContext
	 * @return boolean indicating if the path should be handled
	 * @throws MalformedURLException if the path cannot be used as a URI to open a resource stream
	 */
	public static boolean canHandle(String path, ServletContext context) 
			throws MalformedURLException {

		boolean result = true;
		if (path.length() == 0 || path.equals("/")) {
			if (logger.isDebugEnabled()) {
				logger
						.debug("Empty path detected, this will be passed to default dispatcher");
			}
			result = false;
		} else if (path.contains(".")) {
			if (logger.isDebugEnabled()) {
				logger
						.debug("Found path containing a dot, checking for physical file, path="
								+ path);
			}
			if (context.getResource(path) != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("physical file found for path=" + path
							+ ", forwarding to default dispatcher");
				}
				result = false;
			}
		}
		return result;
	}

	/**
	 * Handles an Action.
	 * 
	 * @param request the HttpServletRequest
	 * @param response the HttpServletResponse
	 * @param path the path
	 * @throws ServletException if anything goes wrong
	 */
	public void handleAction(HttpServletRequest request,
			HttpServletResponse response, String path) throws ServletException {
		try {
			Map<String, Object> map = invoker.invokeActionPath(request,
					response, mappings, path);
			String forwardPath = map.get(EasyPeasConstants.EASYPEA_FORWARD)
					.toString();
			if (forwardPath.endsWith(".vm")) {
				if (logger.isDebugEnabled()) {
					logger.debug("Processing forward as template:"
							+ forwardPath);
				}
				processTemplate(response, map, forwardPath);
			} else {
				if (logger.isDebugEnabled()) {
					logger.debug("Processing forward as path:" + forwardPath);
				}
				map.remove(EasyPeasConstants.EASYPEA_FORWARD);
				map.putAll(invoker.invokePath(request, response, mappings,
						forwardPath));
				selectTemplateAndDisplay(response, map);
			}
		} catch (Exception e) {
			forwardToError(response, e);
		}
	}

	/**
	 * Handles a View.
	 * 
	 * @param request the HttpServletRequest
	 * @param response the HttpServletResponse
	 * @param path the path
	 * @throws ServletException if anything goes wrong
	 */
	public void handleView(HttpServletRequest request,
			HttpServletResponse response, String path) throws ServletException {
		Map<String, Object> resultMap = null;
		try {
			resultMap = invoke(request, response, path);

			// add the handler for importing other EasyPea paths
			resultMap.put(EasyPeasConstants.VIEWHANDER_KEY,
					new ViewHandlerBean(request, response));
			selectTemplateAndDisplay(response, resultMap);
		} catch (Exception e) {
			forwardToError(response, e);
		}
	}

	private void forwardToError(HttpServletResponse response, Throwable e)
			throws ServletException {
		logger.debug(e);
		Map<String, Object> errors = new HashMap<String, Object>();
		errors.put("error", e);
		VelocityContext errorContext = new VelocityContext(errors);

		try {
			Template errorTemplate = engine.getTemplate(errorTemplateName());
			errorTemplate.merge(errorContext, response.getWriter());
		} catch (Exception e1) {
			logger.error("Unable to find error template", e1);
			throw new ServletException(e);
		}
	}

	private String errorTemplateName() {
		return "error.vm";
	}

	private void selectTemplateAndDisplay(HttpServletResponse response,
			Map<String, Object> resultMap) throws ServletException {

		try {
			String templateName = determineTemplateName(resultMap);
			if (logger.isDebugEnabled()) {
				logger.debug("Template name determined as " + templateName);
			}
			processTemplate(response, resultMap, templateName);
		} catch (Exception e1) {
			forwardToError(response, e1);
		}
	}

	private void processTemplate(HttpServletResponse response,
			Map<String, Object> resultMap, String templateName)
			throws Exception, IOException {
		VelocityContext context = new VelocityContext(resultMap);
		Template template = engine.getTemplate(templateName);
		template.merge(context, response.getWriter());
	}

	private Map<String, Object> invoke(HttpServletRequest request,
			HttpServletResponse response, String path)
			throws UnresolvedMethodException, UnknownResourceException,
			ResourceInvocationFailure {
		Map<String, Object> result = null;

		logger.debug("Path=" + path);
		result = invoker.invokePath(request, response, mappings, path);

		return result;
	}

	private String determineTemplateName(Map<String, Object> peaMap)
			throws UnableToDetermineTemplateException {
		return templateFinder.find(peaMap, mappings);
	}

}
